#include <ctype.h>
#include <mem.h>
#include "COMMON.H"

#define VGA_MISC_OUTPUT		0x03C2
#define VGA_SC_INDEX		0x03C4
#define VGA_SC_DATA			0x03C5
#define VGA_CRTC_INDEX		0x03D4
#define VGA_CRTC_DATA		0x03D5
#define VGA_GC_INDEX		0x03CE
#define VGA_GC_DATA			0x03CF
#define VGA_INPUT_STATUS_1	0x03DA

#define Div4(x) (x >> 2)

byte far *ImageData;
byte far *TargetScreen;

void SetVideoMode(byte mode)
{
	union REGS regs;
	regs.h.ah = 0x00;
	regs.h.al = mode;
	int86(0x10, &regs, &regs);
}

void SetVideoModeX(void)
{
	// Based on Abrash's Black Book.
	byte endReg;
	SetVideoMode(0x13);
	outport(VGA_SC_INDEX, 0x0604);
	outport(VGA_SC_INDEX, 0x0100);
	outportb(VGA_MISC_OUTPUT, 0xE3);
	outport(VGA_SC_INDEX, 0x0300);
	outportb(VGA_CRTC_INDEX, 0x11);
	endReg = inportb(VGA_CRTC_DATA);
	endReg &= 0x7F;
	outportb(VGA_CRTC_DATA, endReg);
	outport(VGA_CRTC_INDEX, 0x0D06);
	outport(VGA_CRTC_INDEX, 0x3E07);
	outport(VGA_CRTC_INDEX, 0x4109);
	outport(VGA_CRTC_INDEX, 0xEA10);
	outport(VGA_CRTC_INDEX, 0xAC11);
	outport(VGA_CRTC_INDEX, 0xDF12);
	outport(VGA_CRTC_INDEX, 0x0014);
	outport(VGA_CRTC_INDEX, 0xE715);
	outport(VGA_CRTC_INDEX, 0x0616);
	outport(VGA_CRTC_INDEX, 0xE317);
	outport(VGA_SC_INDEX, 0x0F02);
}

void SetPalette(byte *palette)
{
	short i;
	for (i = 0; i < 256; i++)
	{
		outportb(0x03C8, i);
		outportb(0x03C9, palette[3 * i + 0]);
		outportb(0x03C9, palette[3 * i + 1]);
		outportb(0x03C9, palette[3 * i + 2]);
	}
}

static word ScreenOffset(short screen)
{
	// The three "screen regions" in VGA memory are at offsets:
	//   0x0000, 0x4B00, 0x9600
	Assert(screen >= 0 && screen <= 2, "invalid screen index");
	return screen * 0x4B00;
}

static word ScreenOffsetXY(short screen, short x, short y)
{
	return ScreenOffset(screen) + (y * 80) + Div4(x);
}

void SetDisplayedScreen(short screen)
{
	// This function waits for Display Enable and VSYNC before and after
	// flipping the page to ensure that you will never see video memory
	// that is currently being drawn to.

	// Wait for Display Enable (active low):
	while ((inportb(VGA_INPUT_STATUS_1) & 0x01) != 0) { }

	outportb(VGA_CRTC_INDEX, 0x0C);
	outportb(VGA_CRTC_DATA, HIBYTE(ScreenOffset(screen)));

	// Wait for VSYNC:
	while ((inportb(VGA_INPUT_STATUS_1) & 0x08) == 0) { }
}

void SetTargetScreen(short screen)
{
	TargetScreen = MK_FP(0xA000, ScreenOffset(screen));
}

// Enable fast VGA-to-VGA memory copies using VGA latches.
// (See Listing 48.3 in the Black Book.)
static void VgaLatchCopyOn(void)
{
	// Write to all planes:
	outportb(VGA_SC_INDEX, 0x02);
	outportb(VGA_SC_DATA, 0x0F);
	// Ignore all data from CPU; write values stored in VGA latches:
	outportb(VGA_GC_INDEX, 0x08);
	outportb(VGA_GC_DATA, 0x00);
}

// Disable fast VGA memory copies.
static void VgaLatchCopyOff(void)
{
	// Disable writing from VGA latches:
	outportb(VGA_GC_INDEX, 0x08);
	outportb(VGA_GC_DATA, 0xFF);
}

void CopyScreen(short dstScreen, short srcScreen)
{
	byte far *dst, far *src;
	short planeSize, i;
	dst = MK_FP(0xA000, ScreenOffset(dstScreen));
	src = MK_FP(0xA000, ScreenOffset(srcScreen));
	planeSize = (320 / 4) * 240;
	// Copy, one byte (from each plane) at a time:
	VgaLatchCopyOn();
	asm PUSH CX
	asm PUSH DI
	asm PUSH SI
	asm PUSH DS
	asm LES DI, dst
	asm LDS SI, src
	asm MOV CX, planeSize
	asm CLD
	asm REP MOVSB
	asm POP DS
	asm POP SI
	asm POP DI
	asm POP CX
	VgaLatchCopyOff();
}

void CopyScreenRect(short dstScreen, short srcScreen, short x, short y, short w, short h)
{
	short lineSize, stride, dy, i;
	byte far *dst, far *src;
	dst = MK_FP(0xA000, ScreenOffsetXY(dstScreen, x, y));
	src = MK_FP(0xA000, ScreenOffsetXY(srcScreen, x, y));
	lineSize = Div4(w + 8);
	// Make sure that the line doesn't run off the right side of the screen:
	if (Div4(x) + lineSize > 80) lineSize = 80 - Div4(x);
	stride = 80 - lineSize;
	VgaLatchCopyOn();
	asm PUSH AX
	asm PUSH CX
	asm PUSH DX
	asm PUSH DI
	asm PUSH SI
	asm PUSH DS
	asm LES DI, dst
	asm LDS SI, src
	asm MOV AX, h
	asm MOV DX, stride
	asm CLD
	each_row:
		asm MOV CX, lineSize
		asm REP MOVSB
		asm ADD DI, DX
		asm ADD SI, DX
		asm DEC AX
		asm JNZ each_row
	asm POP DS
	asm POP SI
	asm POP DI
	asm POP DX
	asm POP CX
	asm POP AX
	VgaLatchCopyOff();
}

void ImageInfo(short imageID, short *w, short *h, byte huge **image)
{
	Assert(imageID >= 0 && imageID < IMAGE_COUNT, "invalid image ID");
	if (w) *w = ImageWidths[imageID];
	if (h) *h = ImageHeights[imageID];
	// NOTE: It is essential that the image offset be added to the 
	// huge pointer in a separate step so that the huge pointer
	// will be converted to normalized format.
	if (image)
	{
		*image = ImageData;
		*image += ImageOffsets[imageID];
	}
}

void BlitFullscreen(short imageID)
{
	short plane, y, w, h, mask;
	byte huge *image;

	ImageInfo(imageID, &w, &h, &image);
	Assert(w == 320 && h == 240, "image must be fullscreen-sized");

	MarkRectDirty(0, 0, 320, 240);

	mask = 0x01;
	for (plane = 0; plane < 4; plane++)
	{
		outportb(VGA_SC_INDEX, 0x02);
		outportb(VGA_SC_DATA, mask);
		_fmemcpy(TargetScreen, image, 80 * 240);
		mask <<= 1;
		image += (80 * 240);
	}
}

void Blit(short imageID, short x, short y)
{
	short planeI, imgW, imgH, screenStride, srcStride, dy, planeSize, mask;
	short dstX, dstY, srcX, srcY, w, h;
	short srcPlane, lineWidth, dstSkip, srcSkip;
	byte huge *image, huge *imagePlane;
	byte far *vgaPtr, far *imgPtr;

	ImageInfo(imageID, &imgW, &imgH, &image);

	// Clip the rectangle to draw to the screen bounds.
	dstX = x; dstY = y;
	srcX = 0; srcY = 0;
	w = imgW; h = imgH;
	if (dstX < 0)
	{
		srcX = -x;
		w -= srcX;
		dstX = 0;
	}
	if (dstX + w > 320)
	{
		w = 320 - dstX;
	}
	if (dstY < 0)
	{
		srcY = -y;
		h -= srcY;
		dstY = 0;
	}
	if (dstY + h > 240)
	{
		h = 240 - dstY;
	}

	srcStride = (imgW + 3) / 4;
	planeSize = srcStride * imgH;
	screenStride = 320 / 4;

	// If the image is completely outside the screen bounds, draw nothing.
	if (w <= 0 || h <= 0) return;

	MarkRectDirty(dstX, dstY, w, h);

	mask = 0x01 << (dstX % 4);
	for (planeI = 0; planeI < 4; planeI++)
	{
		// Select the plane to draw from:
		srcPlane = (srcX + planeI) % 4;
		imagePlane = image;
		imagePlane += srcPlane * planeSize;
		// Select the plane to draw to:
		outportb(VGA_SC_INDEX, 0x02);
		outportb(VGA_SC_DATA, mask);
		// When image width is not a multiple of four, some rows of
		//  the image will be one pixel longer than others.
		lineWidth = w / 4;
		if (planeI < (w % 4)) lineWidth++;
		dstSkip = screenStride - lineWidth;
		srcSkip = srcStride - lineWidth;
		// Calculate the initial addresses of source and target data:
		vgaPtr = TargetScreen + dstY * screenStride + (dstX / 4);
		imgPtr = (byte far *)imagePlane + srcY * srcStride + Div4(srcX + planeI);
		// Draw row by row:
		asm PUSH BX
		asm PUSH CX
		asm PUSH DI
		asm PUSH SI
		asm PUSH DS
		asm LES DI, vgaPtr
		asm LDS SI, imgPtr
		asm MOV BX, h
		asm CLD
		each_row:
			asm MOV CX, lineWidth
			asm REP MOVSB
			asm ADD DI, dstSkip
			asm ADD SI, srcSkip
			asm DEC BX
			asm JNZ each_row
		asm POP DS
		asm POP SI
		asm POP DI
		asm POP CX
		asm POP BX
		// After drawing to plane 3, wrap around to plane 0 and bump the
		// dest X coord rightward 4 pixels, to the next plane-aligned column.
		mask <<= 1;
		if (mask == 0x10)
		{
			mask = 0x01;
			dstX += 4;
		}
	}
}

void BlitBlend(short imageID, short x, short y)
{
	short planeI, imgW, imgH, screenStride, srcStride, dx, dy, planeSize, mask;
	short dstX, dstY, srcX, srcY, w, h;
	short srcPlane, lineWidth;
	byte huge *image, huge *imagePlane;
	byte far *vgaPtr, far *imgPtr;

	ImageInfo(imageID, &imgW, &imgH, &image);

	// Clip the rectangle to draw to the screen bounds.
	dstX = x; dstY = y;
	srcX = 0; srcY = 0;
	w = imgW; h = imgH;
	if (dstX < 0)
	{
		srcX = -x;
		w -= srcX;
		dstX = 0;
	}
	if (dstX + w > 320)
	{
		w = 320 - dstX;
	}
	if (dstY < 0)
	{
		srcY = -y;
		h -= srcY;
		dstY = 0;
	}
	if (dstY + h > 240)
	{
		h = 240 - dstY;
	}

	srcStride = (imgW + 3) / 4;
	planeSize = srcStride * imgH;
	screenStride = 320 / 4;

	// If the image is completely outside the screen bounds, draw nothing.
	if (w <= 0 || h <= 0) return;

	MarkRectDirty(dstX, dstY, w, h);

	mask = 0x01 << (dstX % 4);
	for (planeI = 0; planeI < 4; planeI++)
	{
		// Select the plane to draw from:
		srcPlane = (srcX + planeI) % 4;
		imagePlane = image;
		imagePlane += srcPlane * planeSize;
		// Select the plane to draw to:
		outportb(VGA_SC_INDEX, 0x02);
		outportb(VGA_SC_DATA, mask);
		// When image width is not a multiple of four, some rows of
		//  the image will be one pixel longer than others.
		lineWidth = w / 4;
		if (planeI < (w % 4)) lineWidth++;
		// Calculate the initial addresses of source and target data:
		vgaPtr = TargetScreen + dstY * screenStride + (dstX / 4);
		imgPtr = (byte far *)imagePlane + srcY * srcStride + Div4(srcX + planeI);
		// Draw pixel by pixel:
		for (dy = 0; dy < h; dy++)
		{
			for (dx = 0; dx < srcStride; dx++)
			{
				short color = *imgPtr;
				if (color != 255 && dx < lineWidth) *vgaPtr = color;
				imgPtr++;
				vgaPtr++;
			}
			vgaPtr += (screenStride - srcStride);
		}
		mask <<= 1;
		// After drawing to plane 3, wrap around to plane 0 and bump the
		// dest X coord rightward 4 pixels, to the next plane-aligned column.
		if (mask == 0x10)
		{
			mask = 0x01;
			dstX += 4;
		}
	}
}

short DrawTextInner(bool actuallyDraw, char* s, short x, short y)
{
	short x0 = x;
	short y0 = y;
	short measuredWidth = 0;

	for (; *s != 0; s++)
	{
		// Only letters are supported.
		short c, glyph, image, w;
		c = toupper(*s);
		glyph = -1;
		if (c == ' ')
		{
			x += 4;
		}
		else if (c == '\n')
		{
			measuredWidth = Max(measuredWidth, x - x0);
			y += 10;
			x = x0;
		}
		else if (c >= 'A' && c <= 'Z')
		{
			glyph = c - 'A';
		}
		else if (c >= '0' && c <= '9')
		{
			glyph = 26 + c - '0';
		}
		else if (c == ':') glyph = 36;
		else if (c == '.') glyph = 37;
		else if (c == ',') glyph = 38;
		else if (c == '/') glyph = 39;
		else if (c == '-') glyph = 40;
		else if (c == '~') glyph = 41;

		if (glyph >= 0)
		{
			image = FONT_TEXT[glyph];
			ImageInfo(image, &w, NULL, NULL);

			if (actuallyDraw)
			{
				BlitBlend(image, x, y);
			}
			
			x += (w + 1);
		}
	}

	measuredWidth = Max(measuredWidth, x - x0);

	if (actuallyDraw)
	{
		MarkRectDirty(x0, y0, measuredWidth, y - y0 + 10);
	}

	return measuredWidth;
}

void DrawText(char* s, short x, short y)
{
	DrawTextInner(true, s, x, y);
}

void DrawTextCenter(char* s, short x, short y)
{
	short w = DrawTextInner(false, s, x, y);
	DrawTextInner(true, s, x - w / 2, y);
}

void DrawTextRight(char* s, short x, short y)
{
	short w = DrawTextInner(false, s, x, y);
	DrawTextInner(true, s, x - w, y);
}

void FillRect(byte color, short x, short y, short w, short h)
{
	short planeI, screenStride, dy, mask, lineWidth;
	byte far *vgaPtr;

	screenStride = 320 / 4;

	MarkRectDirty(x, y, w, h);

	mask = 0x01 << (x % 4);
	for (planeI = 0; planeI < 4; planeI++)
	{
		// Select the plane to draw to.
		outportb(VGA_SC_INDEX, 0x02);
		outportb(VGA_SC_DATA, mask);
		// When image width is not a multiple of four, some rows of
		//  the image will be one pixel longer than others.
		lineWidth = w / 4;
		if (planeI < (w % 4)) lineWidth++;
		// Calculate the initial addresses of source and target data:
		vgaPtr = TargetScreen + y * screenStride + (x / 4);
		// Draw row by row:
		for (dy = 0; dy < h; dy++)
		{
			_fmemset(vgaPtr, color, lineWidth);
			vgaPtr += screenStride;
		}
		mask <<= 1;
		// After drawing to plane 3, wrap around to plane 0 and bump the
		// dest X coord rightward 4 pixels, to the next plane-aligned column.
		if (mask == 0x10)
		{
			mask = 0x01;
			x += 4;
		}
	}
}
